﻿/*
Copyright (c) 2008 William Duff

Permission is hereby granted, free of charge, to any person obtaining a copy 
of this software and associated documentation files (the "Software"), to deal 
in the Software without restriction, including without limitation the rights 
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all 
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
SOFTWARE.
*/

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;

namespace LiveUpload.YouTube
{
    /// <summary>
    /// Data related to the temporary publishing session.
    /// </summary>
    internal class SessionData
    {
        /// <summary>
        /// Constructs a new SessionData object to encapsulate sessionXml.
        /// </summary>
        /// <param name="sessionXml">
        /// The sessionXml file passed by the IPublishPlugin interface.
        /// </param>
        public SessionData(XmlDocument sessionXml)
        {
            if (sessionXml == null)
            {
                throw new ArgumentNullException("sessionXml");
            }
            this.sessionXml = sessionXml;

            this.publishParametersNode = GetXmlElement("PublishParameters", 
                this.sessionXml.DocumentElement);

            this.developerKey = DeveloperKeyExists() ? GetDeveloperKey() : string.Empty;
            this.clientId = ClientIdExists() ? GetClientId() : string.Empty;
            this.user = UserExists() ? GetUser() : null;
            this.isPublic = PermissionExists() ? GetPermission() : false;
            this.itemSet = ItemSetExists() ? GetItemSet() : new List<Item>();
            this.log = LogExists() ? GetLog() : new List<LogItem>();
        }

        /// <summary>
        /// The sessionXml file passed by the IPublishPlugin interface.
        /// </summary>
        private XmlDocument sessionXml;

        /// <summary>
        /// Points to the PublishParameters XmlNode inside sessionXml.
        /// </summary>
        private XmlNode publishParametersNode;

        // Private variables.
        private string developerKey;
        private string clientId;
        private Account user;
        private bool isPublic;
        private List<Item> itemSet;
        private List<LogItem> log;

        /// <summary>
        /// A unique identifier for the product required as part of the HTTP header to access 
        /// YouTube API features.
        /// </summary>
        public string DeveloperKey
        {
            get { return this.developerKey; }
            set { this.developerKey = value; }
        }

        /// <summary>
        /// An identifier for the product that is passed as a URL parameter to YouTube.
        /// </summary>
        public string ClientId
        {
            get { return this.clientId; }
            set { this.clientId = value; }
        }

        /// <summary>
        /// The YouTube user we wish to upload with.
        /// </summary>
        public Account User
        {
            get { return this.user; }
            set { this.user = value; }
        }

        /// <summary>
        /// A permission for the video. True for public and false for private.
        /// </summary>
        public bool IsPublic
        {
            get { return this.isPublic; }
            set { this.isPublic = value; }
        }

        /// <summary>
        /// The list of items contained in sessionXml that were selected for upload.
        /// </summary>
        public List<Item> ItemSet
        {
            get { return this.itemSet; }
            set { this.itemSet = value; }
        }

        /// <summary>
        /// A log of all successful and unsuccessful uploads.
        /// </summary>
        public List<LogItem> Log
        {
            get { return this.log; }
            set { this.log = value; }
        }

        /// <summary>
        /// Gets an XmlElement, assuming it exists.
        /// </summary>
        /// <param name="elementName">
        /// The name of the element.
        /// </param>
        /// <param name="parentNode">
        /// The immediate parent of the element.
        /// </param>
        /// <returns>
        /// The specified XmlElement.
        /// </returns>
        private XmlElement GetXmlElement(string elementName, XmlNode parentNode)
        {
            if (elementName == null)
            {
                throw new ArgumentNullException("element");
            }
            if (parentNode == null)
            {
                throw new ArgumentNullException("parentNode");
            }

            XmlElement theElement = parentNode.SelectSingleNode(elementName) as XmlElement;
            if (theElement == null)
            {
                throw new ArgumentException(elementName + " does not exist.", "elementName");
            }
            return theElement;
        }

        /// <summary>
        /// Sets an XmlElement, replacing an existing XmlElement if necessary.
        /// </summary>
        /// <param name="element">
        /// The element to set.
        /// </param>
        /// <param name="parentNode">
        /// The immediate parent of the element.
        /// </param>
        private void SetXmlElement(XmlElement element, XmlNode parentNode)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            if (parentNode == null)
            {
                throw new ArgumentNullException("parentNode");
            }

            XmlElement existingElement =
                parentNode.SelectSingleNode(element.LocalName) as XmlElement;
            if (existingElement == null)
            {
                parentNode.AppendChild(element);
            }
            else
            {
                parentNode.ReplaceChild(element, existingElement);
            }
        }

        /// <summary>
        /// Checks to see if a YouTube developer key is present.
        /// </summary>
        /// <returns>
        /// true if a developer key is present and false otherwise.
        /// </returns>
        private bool DeveloperKeyExists()
        {
            XmlElement developerKeyElement =
                this.publishParametersNode.SelectSingleNode("DeveloperKey") as XmlElement;
            return developerKeyElement != null;
        }

        /// <summary>
        /// Gets the YouTube developer key.
        /// </summary>
        /// <returns>
        /// The YouTube developer key.
        /// </returns>
        private string GetDeveloperKey()
        {
            XmlElement developerKeyElement = 
                GetXmlElement("DeveloperKey", this.publishParametersNode);
            return developerKeyElement.InnerText;
        }

        /// <summary>
        /// Sets the YouTube developer key to the specified string.
        /// </summary>
        /// <param name="developerKey">
        /// The developer key to set.
        /// </param>
        private void SetDeveloperKey(string developerKey)
        {
            if (developerKey == null)
            {
                throw new ArgumentNullException("developerKey");
            }

            XmlElement developerKeyElement = this.sessionXml.CreateElement("DeveloperKey");
            developerKeyElement.InnerText = developerKey;

            SetXmlElement(developerKeyElement, this.publishParametersNode);
        }

        /// <summary>
        /// Checks to see if a YouTube client id is present.
        /// </summary>
        /// <returns>
        /// true if a client id is present and false otherwise.
        /// </returns>
        private bool ClientIdExists()
        {
            XmlElement clientIdElement =
                this.publishParametersNode.SelectSingleNode("ClientId") as XmlElement;
            return clientIdElement != null;
        }

        /// <summary>
        /// Gets the YouTube client id.
        /// </summary>
        /// <returns>
        /// The YouTube client id.
        /// </returns>
        private string GetClientId()
        {
            XmlElement clientIdElement = GetXmlElement("ClientId", this.publishParametersNode);
            return clientIdElement.InnerText;
        }

        /// <summary>
        /// Sets the YouTube client ID to the specified string.
        /// </summary>
        /// <param name="clientId">
        /// The client ID to set.
        /// </param>
        private void SetClientId(string clientId)
        {
            if (clientId == null)
            {
                throw new ArgumentNullException("clientId");
            }

            XmlElement clientIdElement = this.sessionXml.CreateElement("ClientId");
            clientIdElement.InnerText = clientId;

            SetXmlElement(clientIdElement, this.publishParametersNode);
        }

        /// <summary>
        /// Checks to see if a user is present.
        /// </summary>
        /// <returns>
        /// true if a user is present and false otherwise.
        /// </returns>
        private bool UserExists()
        {
            XmlElement userElement =
                this.publishParametersNode.SelectSingleNode("User") as XmlElement;
            return userElement != null;
        }

        /// <summary>
        /// Gets the YouTube user to upload with.
        /// </summary>
        /// <returns>
        /// The YouTube user to upload with.
        /// </returns>
        private Account GetUser()
        {
            XmlElement userElement = GetXmlElement("User", this.publishParametersNode);
            XmlElement nameElement = GetXmlElement("Name", userElement);
            XmlElement tokenElement = GetXmlElement("Token", userElement);

            return new Account(nameElement.InnerText, tokenElement.InnerText);
        }

        /// <summary>
        /// Sets the YouTube user to upload with.
        /// </summary>
        /// <param name="user">
        /// The YouTube user to upload with.
        /// </param>
        private void SetUser(Account user)
        {
            if (user == null)
            {
                throw new ArgumentNullException("user");
            }

            XmlElement userElement = this.sessionXml.CreateElement("User");

            XmlElement nameElement = this.sessionXml.CreateElement("Name");
            nameElement.InnerText = user.Name;
            userElement.AppendChild(nameElement);

            XmlElement tokenElement = this.sessionXml.CreateElement("Token");
            tokenElement.InnerText = user.Token;
            userElement.AppendChild(tokenElement);

            SetXmlElement(userElement, this.publishParametersNode);
        }

        /// <summary>
        /// Checks if a permission for the video is set.
        /// </summary>
        /// <returns>
        /// true if a permission is set and false otherwise.
        /// </returns>
        private bool PermissionExists()
        {
            XmlElement permissionElement =
                this.publishParametersNode.SelectSingleNode("Permission") as XmlElement;
            return permissionElement != null;
        }

        /// <summary>
        /// Gets the permission for the video.
        /// </summary>
        /// <returns>
        /// true if the permission is public and false otherwise.
        /// </returns>
        private bool GetPermission()
        {
            XmlElement permissionElement = GetXmlElement("Permission", this.publishParametersNode);
            return permissionElement.GetAttribute("isPublic") == bool.TrueString;
        }

        /// <summary>
        /// Sets the permission for the video.
        /// </summary>
        /// <param name="isPublic">
        /// Permission for the video. True for public and false for private.
        /// </param>
        private void SetPermission(bool isPublic)
        {
            XmlElement permissionElement = this.sessionXml.CreateElement("Permission");
            permissionElement.SetAttribute("isPublic", 
                isPublic ? bool.TrueString : bool.FalseString);

            SetXmlElement(permissionElement, this.publishParametersNode);
        }

        /// <summary>
        /// Checks to see if the item set exists.
        /// </summary>
        /// <returns>
        /// true if item set exists and false otherwise. Should always return true.
        /// </returns>
        private bool ItemSetExists()
        {
            XmlElement itemSetElement =
                this.sessionXml.DocumentElement.SelectSingleNode("ItemSet") as XmlElement;
            return itemSetElement != null;
        }

        /// <summary>
        /// Gets the list of items to upload.
        /// </summary>
        /// <returns>
        /// A list of items to upload.
        /// </returns>
        private List<Item> GetItemSet()
        {
            XmlElement itemSetElement = GetXmlElement("ItemSet", this.sessionXml.DocumentElement);
            XmlNodeList itemNodes = itemSetElement.SelectNodes("Item");

            List<Item> itemSet = new List<Item>();
            foreach (XmlElement itemElement in itemNodes)
            {
                string id = itemElement.GetAttribute("id");
                string filePath = GetXmlElement("FullFilePath", itemElement).InnerText;
                string fileName = GetXmlElement("OriginalFileName", itemElement).InnerText;
                string fileExtension = GetXmlElement("OriginalFileExtension", itemElement).InnerText;
                string perceivedType = GetXmlElement("PerceivedType", itemElement).InnerText;
                string title = GetXmlElement("Title", itemElement).InnerText;

                int width;
                int.TryParse(GetXmlElement("OriginalWidth", itemElement).InnerText, out width);

                int height;
                int.TryParse(GetXmlElement("OriginalHeight", itemElement).InnerText, out height);

                int lengthMS;
                int.TryParse(GetXmlElement("LengthMS", itemElement).InnerText, out lengthMS);

                long fileSizeBytes;
                long.TryParse(GetXmlElement("FileSize", itemElement).InnerText, out fileSizeBytes);
                int fileSizeKb = (int)(fileSizeBytes / 1024);

                List<string> keywordSet = new List<string>();
                XmlNodeList keywords = itemElement.SelectNodes("KeywordSet/Keyword");
                foreach (XmlNode keyword in keywords)
                {
                    keywordSet.Add(keyword.InnerText);
                }

                string description = string.Empty;
                XmlElement descriptionElement = itemElement.SelectSingleNode("Description") as XmlElement;
                if (descriptionElement != null)
                {
                    description = descriptionElement.InnerText;
                }

                string category = string.Empty;
                XmlElement categoryElement = itemElement.SelectSingleNode("Category") as XmlElement;
                if (categoryElement != null)
                {
                    category = categoryElement.InnerText;
                }

                string url = string.Empty;
                XmlElement urlElement = itemElement.SelectSingleNode("Url") as XmlElement;
                if (urlElement != null)
                {
                    url = urlElement.InnerText;
                }

                Item newItem = new Item(id, filePath, fileName, fileExtension, perceivedType, title, 
                    width, height, lengthMS, fileSizeKb, keywordSet, description, category, url);
                itemSet.Add(newItem);
            }

            return itemSet;
        }

        /// <summary>
        /// Set a list of items to upload.
        /// </summary>
        /// <param name="itemSet">
        /// A list of items to upload. The only parameters of an Item that should have changed
        /// are the title, keywordset, description, category and url.
        /// </param>
        private void SetItemSet(List<Item> itemSet)
        {
            if (itemSet == null)
            {
                throw new ArgumentNullException("itemSet");
            }

            XmlElement itemSetElement = GetXmlElement("ItemSet", this.sessionXml.DocumentElement);

            foreach (Item item in itemSet)
            {
                XmlElement itemElement = 
                    GetXmlElement(string.Format("Item[@id='{0}']", item.Id), itemSetElement);

                XmlElement titleElement = this.sessionXml.CreateElement("Title");
                titleElement.InnerText = item.Title;
                SetXmlElement(titleElement, itemElement);

                XmlElement keywordSetElement = this.sessionXml.CreateElement("KeywordSet");
                foreach (string keyword in item.KeywordSet)
                {
                    XmlElement keywordElement = this.sessionXml.CreateElement("Keyword");
                    keywordElement.InnerText = keyword;
                    keywordSetElement.AppendChild(keywordElement);
                }
                SetXmlElement(keywordSetElement, itemElement);

                XmlElement descriptionElement = this.sessionXml.CreateElement("Description");
                descriptionElement.InnerText = item.Description;
                SetXmlElement(descriptionElement, itemElement);

                XmlElement categoryElement = this.sessionXml.CreateElement("Category");
                categoryElement.InnerText = item.Category;
                SetXmlElement(categoryElement, itemElement);

                XmlElement urlElement = this.sessionXml.CreateElement("Url");
                urlElement.InnerText = item.Url;
                SetXmlElement(urlElement, itemElement);
            }
        }

        /// <summary>
        /// Checks to see if the log exists.
        /// </summary>
        /// <returns>
        /// true if the log exists and false otherwise.
        /// </returns>
        private bool LogExists()
        {
            XmlElement logElement =
                this.sessionXml.DocumentElement.SelectSingleNode("Log") as XmlElement;
            return logElement != null;
        }

        /// <summary>
        /// Gets a list of log items.
        /// </summary>
        /// <returns>
        /// A list of log items from the log.
        /// </returns>
        private List<LogItem> GetLog()
        {
            XmlElement logElement = GetXmlElement("Log", this.sessionXml.DocumentElement);
            XmlNodeList logItemNodes = logElement.SelectNodes("Upload");

            List<LogItem> log = new List<LogItem>();
            foreach(XmlElement logItemElement in logItemNodes)
            {
                bool successful = logItemElement.GetAttribute("successful") == bool.TrueString;

                int megabytesUploaded;
                int.TryParse(logItemElement.GetAttribute("uploaded"), out megabytesUploaded);

                int megabytesSize;
                int.TryParse(logItemElement.GetAttribute("size"), out megabytesSize);

                string message = logItemElement.InnerText;
                log.Add(new LogItem(successful, message, megabytesUploaded, megabytesSize));
            }

            return log;
        }

        /// <summary>
        /// Sets a list of log items.
        /// </summary>
        /// <param name="log">
        /// A list of log items.
        /// </param>
        private void SetLog(List<LogItem> log)
        {
            if (log == null)
            {
                throw new ArgumentNullException("log");
            }

            XmlElement logElement = this.sessionXml.CreateElement("Log");

            foreach (LogItem upload in log)
            {
                XmlElement uploadElement = this.sessionXml.CreateElement("Upload");
                uploadElement.InnerText = upload.Message;

                uploadElement.SetAttribute("successful",
                    upload.Successful ? bool.TrueString : bool.FalseString);
                uploadElement.SetAttribute("uploaded", upload.UploadedSize.ToString());
                uploadElement.SetAttribute("size", upload.Size.ToString());

                logElement.AppendChild(uploadElement);
            }

            SetXmlElement(logElement, this.sessionXml.DocumentElement);
        }

        /// <summary>
        /// Sets all of the public properties back into the XmlDocument.
        /// </summary>
        internal void Save()
        {
            SetDeveloperKey(this.developerKey);
            SetClientId(this.clientId);
            SetUser(this.user);
            SetPermission(this.isPublic);
            SetItemSet(this.itemSet);
            SetLog(this.log);
        }
    }
}
